Skip to content

feat: tighten search contract (Phase 8 - DISC-01 + SAFE-01)#91

Merged
PatrickSys merged 4 commits intomasterfrom
feat/phase-8-tighten-search-contract
Apr 9, 2026
Merged

feat: tighten search contract (Phase 8 - DISC-01 + SAFE-01)#91
PatrickSys merged 4 commits intomasterfrom
feat/phase-8-tighten-search-contract

Conversation

@PatrickSys
Copy link
Copy Markdown
Owner

Summary

  • Formalizes a compact/full two-mode contract for search_codebase (DISC-01): compact is now the default, capped at 6 results with light graph context (importedByCount, topExports, layer), top-level budget/patternSummary/bestExample/nextHops, and strongly-relevant memories only. Full mode preserves the previous response shape plus adds budget.
  • Implements freshness-aware edit-readiness gating (SAFE-01): stale index (>7 days) forces a soft-abstain for edit/refactor/migrate intents; aging index (24h-7d) warns; explore intent is never freshness-gated.
  • Removes the duplicate hints.consumers field (was identical to hints.callers).
  • Exposes relevanceReason in results (both modes) - previously computed but never returned.
  • Includes a follow-up test fix for Phase 7: two stale assertions in tests/multi-project-routing.test.ts that were not updated when renderMapMarkdown header changed.

Tests

  • New: tests/search-compact-mode.test.ts (8 tests) - modes, graph context, memory gating, budget, consumers removal
  • New: tests/search-safe-01.test.ts (6 tests) - low-confidence, fresh, aging, stale, explore, propagation
  • Full suite: 426/426 pass
  • pnpm tsc --noEmit: 0 errors

Files changed

  • src/tools/search-codebase.ts - two-mode fork, new helpers, memory filter, graph data wiring
  • src/preflight/evidence-lock.ts - indexFreshness parameter with stale/aging gates
  • src/tools/types.ts - abstain?: boolean added to DecisionCard
  • tests/search-compact-mode.test.ts - new
  • tests/search-safe-01.test.ts - new
  • tests/search-decision-card.test.ts, tests/search-snippets.test.ts - regression fixes (mode: 'full' added to snippet tests)
  • tests/multi-project-routing.test.ts - Phase 7 test fix (map header assertion)

renderMapMarkdown now emits '# Codebase Map — <project>' instead of '# Codebase Intelligence'.
Two stale assertions in tests/multi-project-routing.test.ts were not updated during execution.
Full suite: 412/412 pass.
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Apr 8, 2026

Greptile Summary

This PR formalises a compact/full two-mode contract for search_codebase (DISC-01) and adds freshness-aware edit-readiness gating (SAFE-01). Compact is now the default (≤6 results, light graph context, budget/patternSummary/bestExample/nextHops); full mode preserves the previous shape plus budget. Stale indexes (>7 days) force abstain=true for edit/refactor/migrate intents; aging (24h–7d) warns; explore intent bypasses freshness gating entirely.

  • consumers is removed from the buildRelationshipHints implementation but its declaration is not removed from the exported SearchResultItem.hints type in types.ts, leaving a dead optional field that will never be populated.
  • filterStrongMemories requires ≥2 term matches, so single-keyword symbol searches (e.g. \"AuthService\") silently return no memories in compact mode.
  • readyToEdit in evidence-lock.ts doesn't independently guard against indexFreshness === 'stale'; it relies on the side-effect of status = 'block' being set earlier in the same function.

Confidence Score: 4/5

Safe to merge — all 426 tests pass, logic is correct, findings are P2 clean-ups and defensive suggestions.

The compact/full fork and freshness gating are well-tested (14 new integration tests) and the logic is sound. The three flagged issues are all P2: a stale type field, a defensive readyToEdit guard, and a memory-filter edge case for single-term queries. None are blockers.

src/tools/types.ts (stale consumers field) and src/tools/search-codebase.ts (filterStrongMemories single-term gap)

Vulnerabilities

No security concerns identified. The freshness gating logic operates entirely on local file timestamps with no external input. The new indexFreshness parameter is an internal enum string with no user-controlled surface. No new auth/authz paths, injection vectors, secret handling, or data-exposure risks were introduced.

Important Files Changed

Filename Overview
src/tools/search-codebase.ts Main change: compact/full mode fork, new graph helpers (getImportedByCount, getTopExports, buildNextHops), memory relevance filter, and freshness signal wiring. Minor: filterStrongMemories silently drops memories for single-term queries.
src/preflight/evidence-lock.ts Adds indexFreshness parameter with stale/aging gate logic. Stale forces status = 'block'; aging adds a gap warning. readyToEdit doesn't independently check freshness — relies on side-effect of status override.
src/tools/types.ts Adds abstain?: boolean to DecisionCard. However, consumers is not removed from SearchResultItem.hints despite being dropped from the implementation.
tests/search-safe-01.test.ts 6 integration tests covering low-confidence, fresh, aging, stale, explore-bypass, and gap-propagation. Good fixture isolation with temp dirs and hoisted search mocks.
tests/search-compact-mode.test.ts 8 tests covering result capping, graph context fields, hints/consumers absence, budget/patternSummary/bestExample/nextHops, and memory gating. Solid fixture setup.
tests/multi-project-routing.test.ts Two assertion strings updated from '# Codebase Intelligence' to '# Codebase Map' to match header change from a prior phase. Straightforward fix.
tests/search-decision-card.test.ts Regression fix: mode: 'full' added to snippet-dependent test calls so they use the full response shape (which includes hints/snippet fields).
tests/search-snippets.test.ts Regression fix: mode: 'full' added to ensure snippet tests exercise the full-mode response path.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A([search_codebase called]) --> B{mode param?}
    B -- compact / default --> C[Slice results to ≤6]
    B -- full --> D[All results]

    A --> E{intent?}
    E -- edit / refactor / migrate --> F[Full preflight path]
    E -- explore / none --> G[Lite preflight path]

    F --> H[computeIndexConfidence]
    H --> I{indexFreshness?}
    I -- fresh --> J[buildEvidenceLock — no freshness gate]
    I -- aging --> K[buildEvidenceLock + aging gap added]
    I -- stale --> L[buildEvidenceLock + status forced to block]

    J & K & L --> M{evidenceLock.status == block or searchQuality == low_confidence?}
    M -- yes --> N[decisionCard.abstain = true, ready = false]
    M -- no --> O[decisionCard.ready = true]

    G --> P[buildEvidenceLock — indexFreshness omitted]
    P --> Q[lite preflight: ready + reason only]

    C --> R[Compact response: budget, patternSummary, bestExample, nextHops, importedByCount, topExports]
    D --> S[Full response: budget, hints, relatedMemories]

    N & O --> R
    N & O --> S
Loading

Comments Outside Diff (2)

  1. src/preflight/evidence-lock.ts, line 222-225 (link)

    P2 readyToEdit doesn't independently guard against indexFreshness === 'stale'

    readyToEdit relies entirely on status === 'pass' — which is correct now because the stale branch sets status = 'block' earlier. But if the stale gate is ever reordered (e.g., moved before the epistemic-stress block that can lower 'pass''warn') or refactored, readyToEdit would silently become true for a stale index. Adding an explicit freshness guard makes the contract self-documenting and regression-proof:

  2. src/tools/types.ts, line 83-88 (link)

    P2 Stale consumers field remains in exported type

    The PR removes consumers from the buildRelationshipHints implementation (with the comment "consumers removed — it was identical to callers"), but the exported SearchResultItem.hints interface still declares consumers?: string[]. This creates a dead field: the type says it's allowed but the tool never sets it. Any consumer relying on this field in the SDK/API response will silently get undefined. The field should be removed here to keep the type accurate.

Reviews (1): Last reviewed commit: "feat: implement DISC-01 compact/full sea..." | Re-trigger Greptile

@PatrickSys
Copy link
Copy Markdown
Owner Author

Updated in b549be4: I fixed the stale guard and removed hints.consumers. I left the compact-memory threshold unchanged on purpose and recorded that recall tradeoff as Phase 8 technical debt in .planning/phases/08-tighten-search-contract/08-VERIFICATION.md.

@PatrickSys PatrickSys merged commit 164ff14 into master Apr 9, 2026
3 checks passed
@PatrickSys PatrickSys deleted the feat/phase-8-tighten-search-contract branch April 9, 2026 09:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant